/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */
package poproject.OptimizationTask;

import JaCoP.constraints.Constraint;
import JaCoP.constraints.Sum;
import JaCoP.constraints.SumWeight;
import JaCoP.constraints.XeqC;
import JaCoP.constraints.XmulYeqZ;
import JaCoP.constraints.XplusYeqZ;
import JaCoP.core.BoundDomain;
import JaCoP.core.IntDomain;
import JaCoP.core.IntVar;
import JaCoP.core.IntervalDomain;
import JaCoP.core.Store;
import JaCoP.core.Var;
import JaCoP.search.DepthFirstSearch;
import JaCoP.search.IndomainMin;
import JaCoP.search.Search;
import JaCoP.search.SelectChoicePoint;
import JaCoP.search.SimpleSelect;
import java.util.ArrayList;

/**
 * Silnik do obliczania najlepszej trasy bazujący na bibliotece JaCoP.
 * 
 * Klasa realizuje konwersję modelu mapy do modelu zmiennych i ograniczeń
 * używając bibliotekę JaCoP. Ponadto oferuje metody do rozwiązania zadania
 * optymalizacji polegającego na wyznaczeniu trasy dla której koszt jest minimalny.
 * 
 * @author Eruanno
 */
public class OptimizationTaskEngine {
    
    private MapModel mapModel;
    private MapTrack mapTrack;
    
    /**
     * Magazyn (główny obiekt biblioteki JaCoP).
     */
    private Store store;
    
    /**
     * Lista zmiennych 'finite domain'
     */
    private ArrayList<IntVar> vars;
    private ArrayList<IntVar> xPositions;
    private ArrayList<IntVar> xLevelDeltas;
    private ArrayList<IntVar> xSquares;
    private Search search;
   
    /**
     * Koszt budowy trasy (zmienna FDV).
     */
    private IntVar cost;
    
    public OptimizationTaskEngine(MapModel mapModel) {
        assert(mapModel != null) : "Nie podano obiektu MapModel.";
        
        this.mapModel = mapModel;
    }
    
    public void createModel() {  
        
        store = new Store();
        vars = new ArrayList<>();
        
        // Utwórz listy z zarezerwowaną już liczbą elementów.
        // xPositions - odwzorowania: pozycja -> poziom definiujące wierzchołek trasy
        // xLevelDeltas - przyrosty "w pionie" pomiędzy sąsiednimi pozycjami:
        //                określa charakter przebiegu trasy
        // xSquares - kwadraty zmiennych w xLevelDeltas
        //
        xPositions = new ArrayList<>(mapModel.numOfAreas()+1);
        xLevelDeltas = new ArrayList<>(mapModel.numOfAreas());
        xSquares = new ArrayList<>(mapModel.numOfAreas());
       
        
        // Utwórz zmienne robocze reprezentujące poziomy na których znajdują
        // się wierzchołki trasy.
        for(int i = 0; i <= mapModel.numOfAreas(); i++) {
            IntVar xVar = createPositionVariable(mapModel, i);
            xPositions.add(xVar);
            vars.add(xVar);
        }      
        
        // Utwórz zmienne przyrostów poziomów i ich kwadratów
        // Przyrost może być ujemny, dlatego domena obejmuje skrajne przypadki
        // -(L-1)...(L-1), lub dla kwadratów: 0..(L-1)^2
        for(int i = 1; i <= mapModel.numOfAreas(); i++) {
            int p = mapModel.numOfLevels() - 1;
            IntVar xLevDelta = new IntVar(store, "dX" + i, -p, p);
            IntVar xSquare = new IntVar(store, "dXS" + i, 0, p*p);
            
            xLevelDeltas.add(xLevDelta);
            xSquares.add(xSquare);
            vars.add(xLevDelta);
            vars.add(xSquare);
        }     
        
        //
        // Ograniczenia
        //
        
        // 1. przyrosty poziomów wynikają z pozycji i pozycji poprzedniej
        // deltaX(i+1) = X(i+1) - X(i)  ->  X(i) + deltaX(i+1) = X(i+1)
        // Ponadto tworzymy ograniczenia definiujące kwadraty tych przyrostów
        for (int i = 0; i < mapModel.numOfAreas(); i++) {
            
            IntVar ldelta = xLevelDeltas.get(i);
            
            Constraint temp = new XplusYeqZ(
                    xPositions.get(i),
                    ldelta,
                    xPositions.get(i+1));
            store.impose(temp);
  
            temp = new XmulYeqZ(ldelta, ldelta, xSquares.get(i));
            store.impose(temp);
        }
        
        // 3. Punkty końcowy - końcowy nie są swobodne! Należy je ograniczyć
        store.impose(new XeqC(xPositions.get(0), mapModel.startLevel()));
        store.impose(new XeqC(xPositions.get(mapModel.numOfAreas()), mapModel.finishLevel()));
        
        // 4. Koszt można oszacować jako sumę ważoną kwadratów przyrostów.
        // Wagi odpowiadają kosztom budowy na danym obszarze
        
        ArrayList<Integer> weights = mapModel.areaCosts();
        
        cost = new IntVar(store, "cost", 0, Integer.MAX_VALUE);
        
        Constraint constCstr = new SumWeight(xSquares, weights, cost); 
        store.impose(constCstr);
    }
    
    /**
     * Buduje zmienną reprezentującą poziom na danej pozycji (patrz definicje
     * w dokumentacji klasy MapModel). Domena tej zmiennej ustawiana jest na
     * podstawie obiektu MapObstacle reprezentującego przeszkodę.
     * 
     * @param model Model mapy.
     * @param position Pozycja na mapie, dla której tworzymy zmienną.
     * @return Zmienna FDV, z domeną uwzględniającą strefy zabronione.
     */
    private IntVar createPositionVariable(MapModel model, int position) {
        
        // Tworzymy domenę o zakresie 0 ... poziom maksymalny.
        IntDomain domain = new IntervalDomain(0, model.numOfLevels()-1);

        MapObstacle obst = model.obstacle(position);
        
        if(obst != null)
        {
            // Pobierz niedozwolone poziomy...
            ArrayList<Integer> coords = obst.restrictedCoords();
            
            // ... i uwzględnij je w domenie zmiennej
            for(Integer i : coords) {
                domain.subtractAdapt(i);
            }
        }
        
        // Zwróć zmienną FDV
        return new IntVar(store, "x" + position, domain);
    }
    
    /**
     * Znajduje rozwiązanie.
     * @return Flaga mówiąca o powodzeniu poszukiwań.
     */
    public boolean searchSolution() {
        
        SelectChoicePoint select = new SimpleSelect(vars.toArray(new Var[1]), null,
                new IndomainMin());

        search = new DepthFirstSearch();
        //search.getSolutionListener().searchAll(true);
        search.getSolutionListener().recordSolutions(true);

        boolean result = search.labeling(store, select, cost);
        
        if(result) {
            for(IntVar v : xPositions) {
                System.out.println(v);
            }
        }
        
        return result;
    }
    
    /**
     * @return Wynik obliczeń w postaci trasy.
     */
    public MapTrack getTrackSolution() {
        if(xPositions == null)
            return null;
        
        mapTrack = new MapTrack(mapModel);
        
        for(int i = 0; i <= mapModel.numOfAreas(); i++) {
            mapTrack.setTrackPoint(i, xPositions.get(i).value());
        }
        
        return mapTrack;
    }
}
